<?php

declare(strict_types=1);

namespace MultiWorker;

use Exception;
use MultiWorker\Worker\{Worker,WorkerKeeper};
use MultiWorker\Constract\LoggerInterface;
use MultiWorker\Exception\ProcessException;
use MultiWorker\Logger\Logger;
use Throwable;

class Manager
{
    /**
     * 日志记录对象
     *
     * @var LoggerInterface
     */
    protected $logger;

    /**
     * 当前worker
     *
     * @var Worker[]
     */
    protected $workers = [];

    /**
     * 启动worker数
     *
     * @var integer
     */
    protected $workerNum = 1;

    /**
     * 默认配置
     *
     * @var array
     */
    protected $config = [];

    /**
     * handler.
     *
     * @var mixed
     */
    protected $handler;

    /**
     * Signal manger instance.
     *
     * @var Signal
     */
    protected $signal;
    
    /**
     * process loop sleep time.
     *
     * @var integer
     */
    protected $loopMicrotime = 10000;

    /**
     * 守护进程形式运行
     *
     * @var boolean
     */
    protected $deamon = false;

    /**
     * master pid.
     *
     * @var integer
     */
    protected $masterPid = 0;

    /**
     * master pid save path.
     *
     * @var string
     */
    protected $masterPidPath;

    /**
     * Manager Constructor.
     *
     * @param array $config
     */
    public function __construct(array $config = [])
    {
        $this->config        = array_merge($this->config,$config);
        $this->workerNum     = max(1,$this->config['worker_num'] ?? 1);
        $this->loopMicrotime = $this->config['loop_microtime'] ?? 10000;
        $this->deamon        = $this->config['deamon'] ?? false;
    }

    /**
     * create manager instance.
     *
     * @param array ...$arguments
     * @return Manager
     */
    public static function create (...$arguments): Manager
    {
        return new static(...$arguments);
    }

    /**
     * run
     *
     * @param mixed $handler
     * @return void
     */
    public function run ($handler): void
    {
        $this->setHandler($handler)
             ->deamonize()
             ->forkWorkers()
             ->registerSignalHandler()
             ->process();
    }

    /**
     * 守护进程方式运行
     *
     * @return self
     */
    protected function deamonize (): self
    {   
        if ($this->deamon) {
            $deamon = new Deamon($this->getConfig());
            $deamon->run();
        }
        return $this;
    }

    /**
     * fork worker list.
     *
     * @return self
     */
    protected function forkWorkers (): self
    {
        for ($i = 0; $i < $this->workerNum; $i++) {
            $this->forkOneWorker($i);
        }
        return $this;
    }

    /**
     * fork one worker。
     *
     * @param  integer $workerId
     * @return void
     */
    protected function forkOneWorker (int $workerId = -1): void
    {
        $pid = pcntl_fork();
        switch ($pid) {
            // fork worker error.
            case -1:
                throw new ProcessException('fork worker error.');
                break;
            // child process.
            case 0:
                try {
                    $this->logger()->info('workerid' . $workerId . ':fork -> ' . getmypid());
                    $this->makeWorker($workerId)->process();
                } catch (Exception|Throwable $e) {
                    $this->logger()->error($e->getMessage());
                    throw new ProcessException($e->getMessage());   
                } catch (ProcessException $e) {
                    $this->logger()->error($e->getMessage());
                    throw $e;
                }
                break;
            // parent process.
            default:
                $this->workers[$pid] = new WorkerKeeper($pid,$workerId,$this->getConfig());
                $this->setMasterPid();
                $this->logger()->info('master -> '. $this->getMasterPid());
                break;
        }
    }

    /**
     * 注册信号处理
     *
     * @return self
     */
    protected function registerSignalHandler (): self
    {
        // 信号监听事件
        $this->getSignal()->on('reload',function () {
            foreach ($this->workers as $keeper) {
                $keeper->write(Signal::STATUS_RELOAD);
            }
        });
        $this->getSignal()->on('stop',function () {
            foreach ($this->workers as $keeper) {
                $keeper->write(Signal::STATUS_STOP);
            }
        });
        $this->getSignal()->on('int',function () {
            foreach ($this->workers as $keeper) {
                $keeper->stop();
                $keeper->clear();
            }
            $this->stop();
        });
        $this->getSignal()->on('terminate',function () {
            foreach ($this->workers as $keeper) {
                $keeper->stop();
                $keeper->clear();
            }
            $this->stop();
        });
        // 注册信号监听
        $this->getSignal()->register();
        return $this;
    }

    /**
     * process.
     *
     * @return void
     */
    protected function process (): void
    {
        // 开启异步信号
        $this->getSignal()->openAsyncSignal($this->config['signal_async'] ?? false);
        while (true) {
            if (!$this->isMaster()) {
                return;
            }
            // dispatch signal for the handlers
            $this->getSignal()->dispatch();
            // wait worker process.
            foreach ($this->workers as $keeper) {
                $pid = $keeper->getPid();
                $exitId = pcntl_waitpid($pid,$status,WNOHANG);
                if ($exitId > 0) {
                    $exitCode = intval(pcntl_wexitstatus($status));
                    $this->logger()->warning('workerid exit code:' . $exitCode);
                    $keeper->clear();
                    unset($this->workers[$pid]);
                    switch ($exitCode) {
                        case Signal::STATUS_RELOAD:
                            $this->logger()->info(sprintf('%d worker pid %d process reload.',
                                $keeper->getWorkerId(),$pid
                            ));
                            $this->forkOneWorker($keeper->getWorkerId());
                            break;
                        case Signal::STATUS_STOP:
                            $this->logger()->info(sprintf('%d worker pid %d process stop.',
                                $keeper->getWorkerId(),$pid
                            ));
                            break;
                        default:
                            $this->logger()->error(sprintf('%d worker pid %d process abnormal exit.',
                                $keeper->getWorkerId(),$pid
                            ));                            
                            // 退出自动重启
                            $autoRestart = $this->config['worker_auto_recover'] ?? true;
                            if ($autoRestart) {
                                $this->logger()->info(sprintf('%d worker pid %d auto restart after abnormal exit.',
                                    $keeper->getWorkerId(),$pid
                                ));   
                                $this->forkOneWorker($keeper->getWorkerId());
                            }
                            break;
                    }
                }
            }
            if (empty($this->workers)) {
                $this->stop();
            }
            usleep($this->loopMicrotime);
        }
    }

    /**
     * stop 
     *
     * @return void
     */
    public function stop (): void
    {
        clearstatcache();
        file_exists($this->getMasterPidPath()) && unlink($this->getMasterPidPath());
        exit;
    }

    /**
     * make worker.
     *
     * @param integer $workerId
     * @return Worker
     */
    protected function makeWorker (int $workerId): Worker
    {
        $worker = new Worker($workerId,$this->getHandler(),$this->getConfig());
        $worker->setLogger($this->logger());
        return $worker;
    }

    /**
     * set handler.
     *
     * @param mixed $handler
     * @return self
     */
    public function setHandler ($handler): self
    {
        $this->handler = $handler;
        return $this;
    }

    /**
     * get handler.
     *
     * @return mixed
     */
    public function getHandler ()
    {
        return $this->handler;
    }

    /**
     * get config;
     *
     * @return array
     */
    public function getConfig (): array
    {
        return $this->config;
    }

    /**
     * get signal instance.
     *
     * @return Signal
     */
    public function getSignal (): Signal
    {
        if (!$this->signal) {
            $this->signal = new Signal();
        }
        return $this->signal;
    }

    /**
     * get runtime path.
     *
     * @return string
     */
    public function getRuntimePath ($path = ''): string
    {
        $runtime = $this->config['runtime_path'] ?? null;
        $runtime = $runtime ?: sys_get_temp_dir();
        return $runtime . DIRECTORY_SEPARATOR . $path;
    }

    /**
     * 是否为master进程
     *
     * @return boolean
     */
    public function isMaster (): bool
    {
        return $this->getMasterPid() === getmypid(); 
    }

    /**
     * get master pid.
     *
     * @return integer
     */
    public function getMasterPid (): int
    {
        return $this->masterPid;
    }
    
    /**
     * set master pid.
     *
     * @param integer|null $pid
     * @return self
     */
    public function setMasterPid (?int $pid = null): self
    {
        if (is_null($pid)) {
            $pid = getmypid();
        }
        if ($pid) {
            file_put_contents($this->getMasterPidPath(),$pid);
        }
        $this->masterPid = $pid;
        return $this;
    }

    /**
     * get master pid path.
     *
     * @return string
     */
    protected function getMasterPidPath (): string
    {
        if (!$this->masterPidPath) {
            $this->masterPidPath = $this->config['master_pid_path'] ?? $this->getRuntimePath(
                'multi-worker-master.pid'
            );
            is_dir(dirname($this->masterPidPath)) || mkdir(dirname($this->masterPidPath),0755,true); 
        }
        return $this->masterPidPath;
    }

    /**
     * get logger instance.
     *
     * @return Logger
     */
    public function logger (): LoggerInterface
    {
        if (!$this->logger) {
            $this->logger = Logger::new($this->config['logger']);
        }
        return $this->logger;
    }
}